[JAVA] 인터페이스 vs 추상클래스

업데이트:



2023.06.15 내용추가

이전 포스팅에서 인터페이스추상클래스에 대해서 알아봤다. 그런데 각각의 기능이나 역할은 잘 알겠는데 둘의 차이점을 설명하라고 하면 명확하게 할 수 있을까? 본인같은 경우에도 둘의 차이점을 제대로 설명해보라고 하면 헷갈릴 만한 부분이 분명히 있기 때문에 오늘은 인터페이스와 추상클래스의 공통점과 차이점에 대해서 자세히 포스팅 해보려고 한다!





✅ 인터페이스와 추상클래스의 공통점

  1. 객체지향의 다형성을 활용할 수 있다.

  2. 추상메소드를 사용한다.

  3. 구현부가 없는 추상메소드 때문에 인스턴스화( 객체생성 )가 불가능하다.

  4. 상속받는 클래스 혹은 구현하는 인터페이스 안에 있는 추상메소드를 구현하도록 강제 한다.


이렇게 네가지 정도의 공통점을 들 수 있다. 여기서 몇가지 의문이 든다.

인터페이스는 일반적으로 상수, 추상메소드, 디폴트메소드, 정적 메소드 로 이루어진다. 추상클래스는 단순히 추상메소드를 한개이상 포함한 클래스를 말한다.

그렇다면 추상클래스에 원하는 추상메소드를 여러개 선언해서 상속받아 사용하고, 상수, 디폴트메소드, 정적메소드 역시 그냥 추상클래스에 선언해서 사용하면 추상클래스가 인터페이스의 역할을 전부다 할 수 있게 되는것 아닌가?

반은 맞고 반은 틀린 말이다.

인터페이스와 추상클래스는 존재 목적이 다르다. 추상클래스그 추상클래스를 상속받아서 기능을 이용하고, 확장시키는 데 있다. 반면에 인터페이스는 구현부가 전혀없이 메소드의 껍데기만 있는데, 그 이유는 메소드의 구현을 강제하기 위함이다. 구현을 강제함으로써 구현 객체의 같은 동작을 보장 할 수 있다.





인터페이스와 추상클래스의 차이점

📕 접근제어자

🔔 인터페이스

상수 : public static final

추상메소드 : public abstract

디폴트 메소드 : default

정적메소드 : static



🔔 추상클래스

static , final 이 아닌 필드 사용가능

public, private, protected 모두 사용 가능




인터페이스에서 사용되는 접근제어자는 위와 같다.

인터페이스에서 상수는 항상 고정값으로 적용되기 때문에 final 필드를 가지고 객체생성 없이 사용할 수 있게 끔 static 필드를 가진다.

그러나 추상클래스static 이나 final 필드를 가질 수 없고 public, private, protected 모두 사용 가능하다.




📕 사용 의도의 차이

인터페이스는 HAS - A “ ~을 포함하는 / ~을 할 수 있는” 추상클래스는 IS - A “~이다”

일반적으로 인터페이스와 추상클래스를 구분할 때 HAS -AIS-A 로 구분하곤 한다. 다중상속 가능 여부에 따라 이렇게 구분한 것으로 보이는데, JAVA는 한개의 클래스만 상속이 가능하기 때문에 추상클래스의 상속을 통해 필요한 기능을 사용, 확장하고 인터페이스를 통해 필요한(할수있는) 기능을 강제한다.

글로만 보면 어려우니 예제를 통해 알아보자.





🔔 CalAbstract : 추상클래스

package calculate_Ver_Interface;

import lombok.Data;

@Data
public abstract class CalAbstract {
	
	private int a;
	private int b;
	private String op;
	private double sum=0;
	
	private StringBuffer sb = new StringBuffer();
	private double result=0;
	
	
	private CalResult calResult;
	
	
	// 추상클래스 내에 클래스를 하나 만들어서 인터페이스를 implements해줌으로써 
	// calResult를 초기화할 수 있음.
	// 이게 어댑터 패턴(클래스)이네.. (아님 x)
	class DefaultCalResultDisplay implements CalResult{
		
		@Override
		public void PrintResult() {
			System.out.println("결과 : " + getResult());
		}
		
		@Override
		public void PrintProcess() {
			System.out.println("연산 과정 : " + getSb());
		}
	}
	

	public void add() {
		setSum(getSum()+getA());
		setResult(getSum());
	}
	public void sub() {
		setSum(getSum()-getA());
		setResult(getSum());
	}
	public void mul() {
		setSum(getSum()*getA());
		setResult(getSum());
	}
	public void div() {
		setSum(getSum()/getA());
		setResult(getSum());
	}
	public char judgeOp(String op) {
		if (op.equals("+")) {
			return op.charAt(0);
		} else if (op.equals("-")) {
			return op.charAt(0);
		} else if (op.equals("*")) {
			return op.charAt(0);
		} else if (op.equals("/")) {
			return op.charAt(0);
		}
		return 'X';
	}
	
	// 실제 계산 수행
	public void calculate() {
		if (getOp().equals("+")) {
			add();
		} else if (getOp().equals("-")) {
			sub();
		} else if (getOp().equals("*")) {
			mul();
		} else if (getOp().equals("/")) {
			div();
		}
	}

	// 입력받은게 숫자인지 기호인지 판단하는 객체
	public boolean judgeCal(boolean temp) {
		if (temp == true)
			return true;
		else
			return false;
	}

	// 숫자면 true, 기호면 false return
	public boolean judgeNumOrOp(String temp) {
		return temp.matches("-?\\d+");
	}

	
	// 추상메소드
	public abstract void cal();
	

	
	public void addDisplayResult(CalResult calResult) {
		this.calResult = calResult;
	}
	
	public void display() {
		if(this.calResult == null) {
			DefaultCalResultDisplay defaultCalResultDisplay = new DefaultCalResultDisplay();
			defaultCalResultDisplay.PrintProcess();
			defaultCalResultDisplay.PrintResult();
		}
		else {
			calResult.PrintProcess();
			calResult.PrintResult();
		}
	}

}


사칙연산을 계산해주는 계산기를 위한 추상클래스이다. 객체지향 공부를 목적으로 짜고있는 코드이므로 좀 복잡해보일 수 있다.. 일단 위 코드의 최종 목적을 간단하게 설명해보면 추상클래스와 인터페이스를 활용해서 다른기능을 가진 2개의 계산기를 만드는 것이다.

하나는 일반적인 계산을 수행한다. (ex. 1+1, 2x8, 10/2) 다른 하나는 연속적인 계산을 수행한다. ( ex 5+8-9*2/3+8 ) ( 입력순서대로 ) 최종적으로 연산과정과 연산 결과를 공통적으로 출력한다.

위와 같은 기능을 가진 2개의 계산기를 구현하기 위해 인터페이스와 추상클래스를 어떻게 활용했는지를 중심으로 살펴보자.




추상클래스에 필요한 변수일반 메소드를 먼저 구현해줬다.

	private int a;
	private int b;
	private String op;
	private double sum=0;
	
	// 연산과정을 저장하는 StringBuffer
	private StringBuffer sb = new StringBuffer();
	// 계산 결과
	private double result=0;
	public void add() {
		setSum(getSum()+getA());
		setResult(getSum());
	}
	public void sub() {
		setSum(getSum()-getA());
		setResult(getSum());
	}
	public void mul() {
		setSum(getSum()*getA());
		setResult(getSum());
	}
	public void div() {
		setSum(getSum()/getA());
		setResult(getSum());
	}

... 이하생략




일반 클래스와 다를게 아예없다. 단지 추상클래스에는 추상메소드가 포함되었을 뿐이다.

	// 추상메소드
	public abstract void cal();

이 추상메소드는 이후에 상속받을 클래스에서 각각 재정의 되어 서로 다른 두개의 계산기능(일반계산, 연속계산)으로 구현될 것이다. 추상클래스의 확장 개념이 적용된 것이다.





🔔 CalResult : 인터페이스

package calculate_Ver_Interface;

public interface CalResult {
	// 결과출력 추상메소드
	void PrintResult();
	
	// 과정출력 추상메소드
	void PrintProcess();
}

계산기는 반드시 결과가 출력 되어야만 한다. 위 코드에서는 연산과정과 계산결과를 출력하는게 목적이기 때문에 두개의 메소드를 인터페이스에 선언하였다. 두개의 메소드를 인터페이스에 선언함으로써 두개의 계산기 모두 위 인터페이스를 implements 하여 동일한 형태의 결과출력을 강제하는 효과를 가진다.





🔔 ContinuousCal : 추상클래스를 상속받는 연속계산 클래스

package calculate_Ver_Interface;

import java.util.Scanner;
import java.util.StringTokenizer;

import lombok.Data;

@Data
public class ContinuousCal extends CalAbstract{
	
	Scanner sc = new Scanner(System.in);

	// 연속 계산 수행
	@Override
	public void cal() {
		while (true) {
			String str;
			str = sc.nextLine();

			// 필요한 계산코드인데 생략
				
				display();
				
				
			// 필요한 계산코드인데 생략
		}
	}

위의 코드는 연속계산을 수행하는 클래스로 추상클래스를 상속받아 public void cal() 을 오버라이딩 하여 사용했다. 코드는 너무 길어서 생략했다.




🔔 StandardCal : 추상클래스를 상속받는 일반계산 클래스

package calculate_Ver_Interface;

import java.util.Scanner;
import java.util.StringTokenizer;

import lombok.Data;

@Data
public class StandardCal extends CalAbstract{
	
	Scanner sc = new Scanner(System.in);

	// 단일 계산 수행
	@Override
	public void cal() {
		while(true) {
			String str;
			str = sc.nextLine();
			// 코드 이하 생략
					display();
			// 코드 이하 생략
		}
	}

마찬가지로 추상클래스를 상속받아 public void cal() 을 오버라이딩 한 일반 계산 클래스이다. 코드를 지금 생략해놔서 그렇지 두 클래스의 public void cal() 코드는 서로 기능에 따라 다르게 구성되어 있다.

다시 한번 언급하고 정리해보면 추상클래스에서 선언한 추상메소드 public abstract void cal() 을 두개의 계산기 클래스에서 서로 기능에 맞게끔 다르게 오버라이딩 하여 확장 한 것이다.

그런데 계산기에서 결과를 출력해주는 인터페이스를 implements 하지 않았다. 원래는 그냥 implements 해버리고 결과출력 메소드 오버라이딩해서 결과출력 하면 되는데 그러면 너무 쉽게 해버리는거여서 살짝 공부도 해볼겸 다르게 해봤다.




🔔 CalAbstract : 추상클래스내에 클래스 선언후 implements

private CalResult calResult;
	
	
	// 추상클래스 내에 클래스를 하나 만들어서 인터페이스를 implements해줌으로써 
	// calResult를 초기화할 수 있음.
	class DefaultCalResultDisplay implements CalResult{
		
		@Override
		public void PrintResult() {
			System.out.println("결과 : " + getResult());
		}
		
		@Override
		public void PrintProcess() {
			System.out.println("연산 과정 : " + getSb());
		}
	}
	public void addDisplayResult(CalResult calResult) {
		this.calResult = calResult;
	}
	
	public void display() {
		if(this.calResult == null) {
			DefaultCalResultDisplay defaultCalResultDisplay = new DefaultCalResultDisplay();
			defaultCalResultDisplay.PrintProcess();
			defaultCalResultDisplay.PrintResult();
		}
		else {
			calResult.PrintProcess();
			calResult.PrintResult();
		}
	}

implements를 이런 방식으로도 할 수 있구나 정도로 알아두면 좋을것 같다. 실무에서는 이런방식으로 implements를 정말 많이 사용한다.





정리

⚡ 인터페이스와 추상클래스는 둘다 추상메소드의 구현을 강제한다.

⚡ 인터페이스와 추상클래스는 둘다 인스턴스화가 불가능하다

⚡ 인터페이스와 추상클래스는 접근제어자의 사용범위가 다르다.

⚡ 인터페이스는 메소드의 동일한 동작 강제, 추상클래스는 메소드의 기능 이용, 확장이 주된 목적이다.

⚡ 인터페이스는 HAS-A, 추상클래스는 IS-A 개념

댓글남기기